/*  PLUTO JS -Scripting Web Server.
    Copyright (C) 2013 vivek gangadharan

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package pluto.core;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.tools.shell.Global;
import static pluto.core.AppLogger.*;
import pluto.http.HTTPSession;
import pluto.js.api.App;

public class Host implements Runnable {

    private ApplicationConfiguration config;
    private Thread currentThread;
    private Global global;
    private Cache cache;
    private ServerSocket serverSocket;
    private ExecutorService requestHandlerThreadPool;
    private boolean isShutdown = false;
    private boolean isListening = false;

    public Host(ApplicationConfiguration config) throws Exception {
        this.config = config;
        this.cache = new Cache(config, this);
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                criticalShutdown();
            }
        }));
    }

    @Override
    public void run() {
        currentThread = Thread.currentThread();
        if (config.has(ConfigurationParameter.app)) {
            if (!config.has(ConfigurationParameter.root)) {
                config.root(config.app().getParent());
            }
            try {

                runjsFile();

            } catch (IOException ex) {
                log.log(Level.SEVERE, Localizer.getMessage(Message.SourceError), ex);
                criticalShutdown();
            } catch (Exception ex) {
                log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorConnectingClusterGroup), ex);
                criticalShutdown();
            }

        } else if (config.has(ConfigurationParameter.root)) {
            File file = new File(config.root(), "app.js");
            if (file.exists() && file.isFile()) {
                config.app(file);
                try {
                    runjsFile();
                } catch (IOException ex) {
                    log.log(Level.SEVERE, Localizer.getMessage(Message.SourceError), ex);
                    criticalShutdown();
                } catch (Exception ex) {
                    log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorConnectingClusterGroup), ex);
                    criticalShutdown();
                }
            } else {
                log.log(Level.SEVERE, Localizer.getMessage(Message.NoSourceFound));
            }
        } else {
            log.log(Level.SEVERE, Localizer.getMessage(Message.NoSourceFound));
        }

    }

    private void runjsFile() throws IOException, Exception {
        init();
        Context context = Context.enter();
        try {
            global = new Global(context);
            global.put("app", global, new App(this));
            if (config.has(ConfigurationParameter.lib)) {
                Thread.currentThread().setContextClassLoader(config.lib());
            }
            context.evaluateReader(global, new FileReader(config.app()), "<cmd>", 0, null);
            this.config.connect();
        } finally {
            Context.exit();
        }
    }

    private void init() throws MalformedURLException, IOException {
        //web
        if (!config.has(ConfigurationParameter.web)) {
            File web = new File(config.root(), "web");
            if (web.exists() && web.isDirectory()) {
                config.web(web);
            }
        }
        //lib
        if (!config.has(ConfigurationParameter.lib)) {
            File lib = new File(config.root(), "lib");
            if (lib.exists() && lib.isDirectory()) {
                config.lib(lib);
            }
        }
        //log
        if (!config.has(ConfigurationParameter.log)) {
            File log = new File(config.root(), "log");
            if (log.exists() && log.isDirectory()) {
                config.log(log);
            }
        }
        //scriptlet
        if (!config.has(ConfigurationParameter.jss)) {
            File log = new File(config.root(), "jss");
            if (log.exists() && log.isDirectory()) {
                config.jss(log);
            }
        }
    }

    public void start() throws UnknownHostException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
        if (!config.has(ConfigurationParameter.host)) {
            InetAddress address;
            try {
                address = InetAddress.getByName("localhost");
                config.host(address);
            } catch (UnknownHostException ex) {
                log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorServerStart), ex);
                throw ex;
            }

        }

        if (config.has(ConfigurationParameter.sslKeyStroke)) {
            try {
                serverSocket = Util.https(config.host(), config.port(), ApplicationConfiguration.SocketBlackLog, config.sslKeyStroke(), config.sslStorePassword(), config.sslKeyStrokePassword(), "SunX509");
            } catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException ex) {
                log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorServerStart), ex);
                throw ex;
            }
        } else {
            serverSocket = Util.http(config.host(), config.port(), ApplicationConfiguration.SocketBlackLog);
        }
        if (config.port() == -1) {
            config.port(serverSocket.getLocalPort());
        }
        this.requestHandlerThreadPool = Executors.newFixedThreadPool(config.requestHandlerPool());
        final Host host = this;
        Executors.newFixedThreadPool(1).submit(
                new Runnable() {
                    @Override
                    public void run() {
                        isListening = true;
                        log.log(Level.INFO, "Listening " + config.host().getHostName() + ":" + config.port());
                        while (!isShutdown) {
                            try {
                                Socket client = serverSocket.accept();
                                requestHandlerThreadPool.submit(new HTTPSession(host, config, client));
                            } catch (IOException ex) {
                                log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorAcceptingClientSocket), ex);
                            }
                        }
                    }
                });
    }

    public synchronized void smoothShutdown() {
        this.isShutdown = true;
        shutdown();
    }

    public ApplicationConfiguration config() {
        return config;
    }

    private synchronized void criticalShutdown() {
        shutdown();
    }

    private synchronized void shutdown() {
        log.log(Level.ALL, "Shutting down server");
        try {
            serverSocket.close();
        } catch (IOException ex) {
            if (!serverSocket.isClosed()) {
                log.log(Level.SEVERE, "Error server socket close", ex);
            }
        }
        Util.shutdownService(requestHandlerThreadPool);
        config.cluster().close();
    }

    public Cache cache() {
        return cache;
    }

    public boolean listening() {
        return isListening;
    }

    public Global global() {
        return global;
    }
}
